using System;

namespace GameFrame.Deterministic
{
    public interface IDeterministicCommandPool
    {
        Type CommandType { get; }

        DeterministicCommand Acquire();

        bool Release(DeterministicCommand cmd);
    }
    
      public abstract class DeterministicFrame
  {
    public const int DumpFlag_NoHeap = 1;
    internal DeterministicFrameInput Input;
    internal bool Verified;
    public int Number;

    public bool IsVerified => this.Verified;

    public bool IsPredicted => !this.Verified;

    public void Init(int playerCount, int inputSize, Native.Allocator allocator)
    {
      if (this.Input == null)
        this.Input = new DeterministicFrameInput(playerCount, inputSize, allocator);
      else
        this.Input.Clear();
    }

    internal DeterministicInputFlags GetPlayerInputFlagsInternal(int player) =>
      player >= 0 && player < this.Input.PlayerCount ? this.Input.Flags[player] : (DeterministicInputFlags)0;

    public byte[] GetRawRpc(int player) =>
      player >= 0 && player < this.Input.PlayerCount ? this.Input.Rpcs[player] : (byte[])null;

    public unsafe byte* GetRawInput(int player) => player >= 0 && player < this.Input.PlayerCount
      ? this.Input.GetPlayerInput(player)
      : (byte*)null;

    public DeterministicInputFlags GetPlayerInputFlags(int player) =>
      this.GetPlayerInputFlagsInternal(player) & ~DeterministicInputFlags.Repeatable;

    public DeterministicCommand GetPlayerCommand(int player) => player >= 0 && player < this.Input.PlayerCount
      ? this.Input.Cmds[player]
      : (DeterministicCommand)null;

    public abstract string DumpFrame(int dumpFlags = 0);

    public abstract ulong CalculateChecksum();

    public abstract void Free();

    public abstract byte[] Serialize(DeterministicFrameSerializeMode mode);

    public abstract void Deserialize(byte[] data);

    public void CopyFrom(DeterministicFrame frame)
    {
      this.Copy(frame);
      this.Number = frame.Number;
    }

    protected abstract void Copy(DeterministicFrame frame);
  }

  internal class DeterministicFrameInput
  {
    private int _playerCount;
    private int _inputSize;
    public byte[][] Rpcs;
    public DeterministicCommand[] Cmds;
    public DeterministicInputFlags[] Flags;
    public unsafe byte* Input;

    public int PlayerCount => this._playerCount;

    public int InputSize => this._inputSize;

    public unsafe void Clear()
    {
      Native.Utils.Clear((void*)this.Input, this._playerCount * this._inputSize);
      for (int index = 0; index < this.Rpcs.Length; ++index)
        this.Rpcs[index] = (byte[])null;
      for (int index = 0; index < this.Flags.Length; ++index)
        this.Flags[index] = (DeterministicInputFlags)0;
      for (int index = 0; index < this.Cmds.Length; ++index)
      {
        this.Cmds[index]?.Dispose();
        this.Cmds[index] = (DeterministicCommand)null;
      }
    }

    public unsafe byte* GetPlayerInput(int player)
    {
      Assert.Check(player >= 0 && player < this.PlayerCount);
      return this.Input + player * this._inputSize;
    }

    public unsafe DeterministicFrameInput(
      int playerCount,
      int inputSize,
      Native.Allocator allocator)
    {
      Assert.Check(playerCount > 0);
      Assert.Check(inputSize > 0);
      this._playerCount = playerCount;
      this._inputSize = inputSize;
      this.Rpcs = new byte[playerCount][];
      this.Cmds = new DeterministicCommand[playerCount];
      this.Flags = new DeterministicInputFlags[playerCount];
      this.Input = (byte*)allocator.AllocAndClear(playerCount * inputSize);
    }

    public void DeserializeCommands(DeterministicCommandSerializer serializer)
    {
      for (int index = 0; index < this.Rpcs.Length; ++index)
      {
        if ((this.Flags[index] & DeterministicInputFlags.Command) == DeterministicInputFlags.Command)
        {
          if (this.Rpcs[index] == null)
            Log.Error("Received command flag without rpc data set", Array.Empty<object>());
          else if (this.Cmds[index] == null || this.Cmds[index].Source != this.Rpcs[index])
          {
            serializer.CommandSerializerStreamRead.SetBuffer(this.Rpcs[index]);
            serializer.CommandSerializerStreamRead.Reading = true;
            DeterministicCommand cmd;
            if (serializer.ReadNext(serializer.CommandSerializerStreamRead, out cmd))
            {
              this.Cmds[index] = cmd;
              this.Cmds[index].Source = this.Rpcs[index];
            }
            else
              Log.Error("Deserialize failed for command", Array.Empty<object>());
          }
        }
      }
    }

    public unsafe void Assign(DeterministicFrameInputTemp input, IDeterministicGame sessionGame)
    {
      this.Rpcs[input.Player] = input.Rpc;
      this.Flags[input.Player] = input.Flags;
      sessionGame.DeserializeInputInto(input.Player, input.Data, this.GetPlayerInput(input.Player));
    }

    public unsafe void CopyFrom(DeterministicFrameInput from)
    {
      Assert.Check(this.PlayerCount == from.PlayerCount);
      for (int index = 0; index < this.PlayerCount; ++index)
      {
        this.Cmds[index] = from.Cmds[index];
        this.Rpcs[index] = from.Rpcs[index];
        this.Flags[index] = from.Flags[index];
      }

      Native.Utils.Copy((void*)this.Input, (void*)from.Input, this._playerCount * this._inputSize);
    }

    public unsafe void CopyForPrediction(DeterministicFrameInput from, int player)
    {
      this.Flags[player] = from.Flags[player] & ~DeterministicInputFlags.Command;
      Native.Utils.Copy((void*)this.GetPlayerInput(player), (void*)from.GetPlayerInput(player), this._inputSize);
    }

    public unsafe void Free(Native.Allocator allocator)
    {
      this.Rpcs = (byte[][])null;
      this.Flags = (DeterministicInputFlags[])null;
      this.Cmds = (DeterministicCommand[])null;
      if ((IntPtr)this.Input == IntPtr.Zero)
        return;
      allocator.Free((void*)this.Input);
      this.Input = (byte*)null;
    }
  }
  
  public abstract class DeterministicCommand : IDeterministicCommandFactory, IDisposable
  {
    internal byte[] Source;
    public IDeterministicCommandPool Pool;
    internal bool Pooled;

    public virtual void Serialize(BitStream stream, DeterministicCommandSerializer cmdSerializer) => this.Serialize(stream);

    public abstract void Serialize(BitStream stream);

    public Type CommandType => this.GetType();

    public virtual DeterministicCommand GetCommandInstance() => this.Pool != null ? this.Pool.Acquire() : (DeterministicCommand) Activator.CreateInstance(this.GetType());

    public virtual void Dispose() => this.Pool?.Release(this);
  }
  public interface IDeterministicCommandFactory
  {
    Type CommandType { get; }

    DeterministicCommand GetCommandInstance();
  }
  
    public class DeterministicCommandSerializer
  {
    private Dictionary<Type, ushort> _typeToId = new Dictionary<Type, ushort>();
    private Dictionary<ushort, IDeterministicCommandFactory> _idToFactory = new Dictionary<ushort, IDeterministicCommandFactory>();
    public BitStream CommandSerializerStreamRead;
    public BitStream CommandSerializerStreamWrite;

    public DeterministicCommandSerializer()
    {
      this.CommandSerializerStreamRead = new BitStream(8192);
      this.CommandSerializerStreamWrite = new BitStream(8192);
    }

    [Obsolete("Use RegisterFactory instead.")]
    public void RegisterPrototype(DeterministicCommand cmd) => this.RegisterFactory((IDeterministicCommandFactory) cmd);

    [Obsolete("Use RegisterFactories instead.")]
    public void RegisterPrototypes(
      params DeterministicCommand[] deterministicCommands)
    {
      this.RegisterFactories((IDeterministicCommandFactory[]) deterministicCommands);
    }

    public void RegisterFactory(IDeterministicCommandFactory factory)
    {
      if (factory == null || !typeof (DeterministicCommand).IsAssignableFrom(factory.CommandType) || this._typeToId.ContainsKey(factory.CommandType))
        return;
      ushort key = (ushort) (this._idToFactory.Count + 1);
      this._idToFactory.Add(key, factory);
      this._typeToId.Add(factory.CommandType, key);
    }

    public void RegisterFactories(params IDeterministicCommandFactory[] factories)
    {
      foreach (IDeterministicCommandFactory factory in factories)
        this.RegisterFactory(factory);
    }

    public bool PackNext(BitStream s, DeterministicCommand cmd)
    {
      int position = s.Position;
      s.WriteUShort(this._typeToId[cmd.GetType()]);
      cmd.Serialize(s, this);
      if (!s.Overflowing)
        return true;
      s.Position = position;
      return false;
    }

    public bool ReadNext(BitStream s, out DeterministicCommand cmd)
    {
      try
      {
        if (!s.CanRead(16))
        {
          cmd = (DeterministicCommand) null;
          return false;
        }
        ushort key = s.ReadUShort();
        cmd = this._idToFactory[key].GetCommandInstance();
        cmd.Serialize(s, this);
        return true;
      }
      catch
      {
        cmd = (DeterministicCommand) null;
        return false;
      }
    }
  }
}